// 版权所有2011 Go作者。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

package zip

import (
	"bufio"
	"encoding/binary"
	"errors"
	"hash"
	"hash/crc32"
	"io"
	"strings"
	"unicode/utf8"
)

var (
	errLongName  = errors.New("zip: FileHeader.Name too long")
	errLongExtra = errors.New("zip: FileHeader.Extra too long")
)

// Writer实现了一个zip文件编写器。
type Writer struct {
	cw          *countWriter
	dir         []*header
	last        *fileWriter
	closed      bool
	compressors map[uint16]Compressor
	comment     string

	// 如果使用大小调用非nil，则testHookCloseSizeOffset
	// 关闭时中心目录的偏移量。
	testHookCloseSizeOffset func(size, offset uint64)
}

type header struct {
	*FileHeader
	offset uint64
}

// NewWriter返回一个向w写入zip文件的新Writer。
func NewWriter(w io.Writer) *Writer {
	return &Writer{cw: &countWriter{w: bufio.NewWriter(w)}}
}

// SetOffset设置压缩包中zip数据开头的偏移量
// 潜在作者。将zip数据附加到
// 现有文件，例如二进制可执行文件。
// 必须在写入任何数据之前调用它。
func (w *Writer) SetOffset(n int64) {
	if w.cw.count != 0 {
		panic("zip: SetOffset called after data was written")
	}
	w.cw.count = n
}

// 刷新将所有缓冲数据刷新到基础写入程序。
// 通常不需要调用Flush；打电话结束就足够了。
func (w *Writer) Flush() error {
	return w.cw.w.(*bufio.Writer).Flush()
}

// SetComment设置中心目录注释字段的结尾。
// 只能在关闭前调用它。
func (w *Writer) SetComment(comment string) error {
	if len(comment) > uint16max {
		return errors.New("zip: Writer.Comment too long")
	}
	w.comment = comment
	return nil
}

// Close通过写入中心目录来完成zip文件的写入。
// 它不会关闭基础编写器。
func (w *Writer) Close() error {
	if w.last != nil && !w.last.closed {
		if err := w.last.close(); err != nil {
			return err
		}
		w.last = nil
	}
	if w.closed {
		return errors.New("zip: writer closed twice")
	}
	w.closed = true

	// 写入中心目录
	start := w.cw.count
	for _, h := range w.dir {
		var buf [directoryHeaderLen]byte
		b := writeBuf(buf[:])
		b.uint32(uint32(directoryHeaderSignature))
		b.uint16(h.CreatorVersion)
		b.uint16(h.ReaderVersion)
		b.uint16(h.Flags)
		b.uint16(h.Method)
		b.uint16(h.ModifiedTime)
		b.uint16(h.ModifiedDate)
		b.uint32(h.CRC32)
		if h.isZip64() || h.offset >= uint32max {
			// 该文件需要zip64头。将maxint存储在两个
			// 32位大小的字段（以及稍后的偏移量），以表示
			// 应使用zip64额外的收割台。
			b.uint32(uint32max) // 压缩尺寸
			b.uint32(uint32max) // 未压缩大小

			// 将zip64额外块附加到额外块
			var buf [28]byte // 2个uint16+3个uint64
			eb := writeBuf(buf[:])
			eb.uint16(zip64ExtraID)
			eb.uint16(24) // 尺寸=3x uint64
			eb.uint64(h.UncompressedSize64)
			eb.uint64(h.CompressedSize64)
			eb.uint64(h.offset)
			h.Extra = append(h.Extra, buf[:]...)
		} else {
			b.uint32(h.CompressedSize)
			b.uint32(h.UncompressedSize)
		}

		b.uint16(uint16(len(h.Name)))
		b.uint16(uint16(len(h.Extra)))
		b.uint16(uint16(len(h.Comment)))
		b = b[4:] // 跳过磁盘号开始和内部文件属性（2x uint16）
		b.uint32(h.ExternalAttrs)
		if h.offset > uint32max {
			b.uint32(uint32max)
		} else {
			b.uint32(uint32(h.offset))
		}
		if _, err := w.cw.Write(buf[:]); err != nil {
			return err
		}
		if _, err := io.WriteString(w.cw, h.Name); err != nil {
			return err
		}
		if _, err := w.cw.Write(h.Extra); err != nil {
			return err
		}
		if _, err := io.WriteString(w.cw, h.Comment); err != nil {
			return err
		}
	}
	end := w.cw.count

	records := uint64(len(w.dir))
	size := uint64(end - start)
	offset := uint64(start)

	if f := w.testHookCloseSizeOffset; f != nil {
		f(size, offset)
	}

	if records >= uint16max || size >= uint32max || offset >= uint32max {
		var buf [directory64EndLen + directory64LocLen]byte
		b := writeBuf(buf[:])

		// zip64中央目录记录的结尾
		b.uint32(directory64EndSignature)
		b.uint64(directory64EndLen - 12) // 长度减去签名（uint32）和长度字段（uint64）
		b.uint16(zipVersion45)           // 版本由
		b.uint16(zipVersion45)           // 需要提取的版本
		b.uint32(0)                      // 此磁盘的编号
		b.uint32(0)                      // 以中心目录开头的磁盘编号
		b.uint64(records)                // 此磁盘上中心目录中的项目总数
		b.uint64(records)                // 中心目录中的项目总数
		b.uint64(size)                   // 中心目录的大小
		b.uint64(offset)                 // 中心目录的起始位置相对于起始磁盘号的偏移量

		// zip64中央目录定位器的结尾
		b.uint32(directory64LocSignature)
		b.uint32(0)           // 以zip64结尾的中央目录开始的磁盘数
		b.uint64(uint64(end)) // 中心目录记录zip64结尾的相对偏移量
		b.uint32(1)           // 磁盘总数

		if _, err := w.cw.Write(buf[:]); err != nil {
			return err
		}

		// 在常规结束记录中存储最大值以发送信号
		// 应该改用zip64值
		records = uint16max
		size = uint32max
		offset = uint32max
	}

	// 写入结束记录
	var buf [directoryEndLen]byte
	b := writeBuf(buf[:])
	b.uint32(uint32(directoryEndSignature))
	b = b[4:]                        // 跳过磁盘号和第一个磁盘号（2x uint16）
	b.uint16(uint16(records))        // 此磁盘上的条目数
	b.uint16(uint16(records))        // 参赛作品总数
	b.uint32(uint32(size))           // 目录大小
	b.uint32(uint32(offset))         // 目录开头
	b.uint16(uint16(len(w.comment))) // EOCD注释的字节大小
	if _, err := w.cw.Write(buf[:]); err != nil {
		return err
	}
	if _, err := io.WriteString(w.cw, w.comment); err != nil {
		return err
	}

	return w.cw.w.(*bufio.Writer).Flush()
}

// Create使用提供的名称将文件添加到zip文件中。
// 它返回文件内容应写入的写入程序。
// 文件内容将使用Deflate方法进行压缩。
// 名称必须是相对路径：不得以驱动器开头
// 字母（例如C:）或前导斜杠，并且只使用正斜杠
// 允许。要创建目录而不是文件，请添加尾随
// 斜杠到名字。
// 文件的内容必须在下一次操作之前写入io.Writer
// 调用以创建、创建标头或关闭。
func (w *Writer) Create(name string) (io.Writer, error) {
	header := &FileHeader{
		Name:   name,
		Method: Deflate,
	}
	return w.CreateHeader(header)
}

// detectUTF8报告s是否为有效的UTF-8字符串，以及该字符串是否
// 必须考虑UTF-8编码（即不兼容CP-437、ASCII、，
// 或任何其他通用编码）。
func detectUTF8(s string) (valid, require bool) {
	for i := 0; i < len(s); {
		r, size := utf8.DecodeRuneInString(s[i:])
		i += size
		// ZIP正式使用CP-437，但许多读者使用系统的
		// 本地字符编码。大多数编码都与大型计算机兼容
		// CP-437的子集，其本身类似于ASCII。
		// None
		// 禁止0x7e和0x5c，因为EUC-KR和Shift JIS替换了这些
		// 具有本地化货币和下划线字符的字符。
		if r < 0x20 || r > 0x7d || r == 0x5c {
			if !utf8.ValidRune(r) || (r == utf8.RuneError && size == 1) {
				return false, false
			}
			require = true
		}
	}
	return true, require
}

// CreateHeader使用提供的FileHeader将文件添加到zip存档
// 对于文件元数据。作者拥有fh的所有权，并可能发生变异
// 它的领域。调用方在调用CreateHeader后不得修改fh。
// None
// 这将返回文件内容应写入的写入器。
// 文件的内容必须在下一次操作之前写入io.Writer
// 调用以创建、创建标头或关闭。
func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
	if w.last != nil && !w.last.closed {
		if err := w.last.close(); err != nil {
			return nil, err
		}
	}
	if len(w.dir) > 0 && w.dir[len(w.dir)-1].FileHeader == fh {
		// 请参阅https:
		return nil, errors.New("archive/zip: invalid duplicate FileHeader")
	}

	// ZIP格式在字符编码方面有一个糟糕的状况。
	// 根据官方规定，名称和注释字段应该进行编码
	// 在CP-437（主要与ASCII兼容）中，除非UTF-8
	// 设置了标志位。但是，有几个问题：
	// None
	// *许多ZIP阅读器仍然不支持UTF-8。
	// *如果清除UTF-8标志，几个读卡器只需解释
	// 名称和注释字段，无论本地系统编码是什么。
	// None
	// 为了避免在没有UTF-8支持的情况下中断读卡器，
	// 如果字符串与CP-437兼容，我们将避免设置UTF-8标志。
	// 但是，如果字符串需要多字节UTF-8编码，并且是
	// 有效的UTF-8字符串，然后设置UTF-8位。
	// None
	// 对于这种情况，用户明确希望指定编码
	// 作为UTF-8，他们需要自己设置标志位。
	utf8Valid1, utf8Require1 := detectUTF8(fh.Name)
	utf8Valid2, utf8Require2 := detectUTF8(fh.Comment)
	switch {
	case fh.NonUTF8:
		fh.Flags &^= 0x800
	case (utf8Require1 || utf8Require2) && (utf8Valid1 && utf8Valid2):
		fh.Flags |= 0x800
	}

	fh.CreatorVersion = fh.CreatorVersion&0xff00 | zipVersion20 // 保留兼容字节
	fh.ReaderVersion = zipVersion20

	// 如果设置了“修改”，则优先于MS-DOS时间戳字段。
	if !fh.Modified.IsZero() {
		// 与FileHeader.SetModTime方法相反，我们故意
		// 不要转换为UTC，因为我们假设用户打算进行编码
		// 使用指定时区的日期。用户可能需要此控件
		// 因为许多传统的ZIP读取器根据
		// 到当地时区。
		// None
		// 如果用户直接设置修改后的时间，则时区仅为非UTC
		// 他们自己直接进行实地考察。所有其他方法设置UTC。
		fh.ModifiedDate, fh.ModifiedTime = timeToMsDosTime(fh.Modified)

		// 使用“扩展时间戳”格式，因为这是InfoZip使用的格式。
		// 几乎每个主要的ZIP实现都使用不同的格式，
		// 但至少大多数人似乎能够理解其他格式。
		// None
		// 本地和中心标头的格式恰好相同
		// 如果修改时间是编码的唯一时间戳。
		var mbuf [9]byte // 2*SizeOf（uint16）+SizeOf（uint8）+SizeOf（uint32）
		mt := uint32(fh.Modified.Unix())
		eb := writeBuf(mbuf[:])
		eb.uint16(extTimeExtraID)
		eb.uint16(5)  // 尺寸：尺寸（uint8）+尺寸（uint32）
		eb.uint8(1)   // 旗帜：ModTime
		eb.uint32(mt) // 现代
		fh.Extra = append(fh.Extra, mbuf[:]...)
	}

	var (
		ow io.Writer
		fw *fileWriter
	)
	h := &header{
		FileHeader: fh,
		offset:     uint64(w.cw.count),
	}

	if strings.HasSuffix(fh.Name, "/") {
		// 将压缩方法设置为存储，以确保数据长度真正为零，
		// writeHeader方法总是对大小字段进行编码。
		// 这是必要的，因为大多数压缩格式具有非零长度
		// 即使在压缩空字符串时也是如此。
		fh.Method = Store
		fh.Flags &^= 0x8 // 我们不会编写数据描述符

		// 明确清除大小，因为它们对目录没有意义。
		fh.CompressedSize = 0
		fh.CompressedSize64 = 0
		fh.UncompressedSize = 0
		fh.UncompressedSize64 = 0

		ow = dirWriter{}
	} else {
		fh.Flags |= 0x8 // 我们将编写一个数据描述符

		fw = &fileWriter{
			zipw:      w.cw,
			compCount: &countWriter{w: w.cw},
			crc32:     crc32.NewIEEE(),
		}
		comp := w.compressor(fh.Method)
		if comp == nil {
			return nil, ErrAlgorithm
		}
		var err error
		fw.comp, err = comp(fw.compCount)
		if err != nil {
			return nil, err
		}
		fw.rawCount = &countWriter{w: fw.comp}
		fw.header = h
		ow = fw
	}
	w.dir = append(w.dir, h)
	if err := writeHeader(w.cw, fh); err != nil {
		return nil, err
	}
	// 如果我们正在创建一个目录，fw是nil。
	w.last = fw
	return ow, nil
}

func writeHeader(w io.Writer, h *FileHeader) error {
	const maxUint16 = 1<<16 - 1
	if len(h.Name) > maxUint16 {
		return errLongName
	}
	if len(h.Extra) > maxUint16 {
		return errLongExtra
	}

	var buf [fileHeaderLen]byte
	b := writeBuf(buf[:])
	b.uint32(uint32(fileHeaderSignature))
	b.uint16(h.ReaderVersion)
	b.uint16(h.Flags)
	b.uint16(h.Method)
	b.uint16(h.ModifiedTime)
	b.uint16(h.ModifiedDate)
	b.uint32(0) // 由于我们正在编写数据描述符crc32，
	b.uint32(0) // 压缩尺寸，
	b.uint32(0) // 未压缩的大小应为零
	b.uint16(uint16(len(h.Name)))
	b.uint16(uint16(len(h.Extra)))
	if _, err := w.Write(buf[:]); err != nil {
		return err
	}
	if _, err := io.WriteString(w, h.Name); err != nil {
		return err
	}
	_, err := w.Write(h.Extra)
	return err
}

// RegisterCompressor注册或覆盖特定对象的自定义压缩器
// 方法ID。如果找不到给定方法的压缩器，编写器将
// 默认情况下，在包级别查找压缩器。
func (w *Writer) RegisterCompressor(method uint16, comp Compressor) {
	if w.compressors == nil {
		w.compressors = make(map[uint16]Compressor)
	}
	w.compressors[method] = comp
}

func (w *Writer) compressor(method uint16) Compressor {
	comp := w.compressors[method]
	if comp == nil {
		comp = compressor(method)
	}
	return comp
}

type dirWriter struct{}

func (dirWriter) Write(b []byte) (int, error) {
	if len(b) == 0 {
		return 0, nil
	}
	return 0, errors.New("zip: write to directory")
}

type fileWriter struct {
	*header
	zipw      io.Writer
	rawCount  *countWriter
	comp      io.WriteCloser
	compCount *countWriter
	crc32     hash.Hash32
	closed    bool
}

func (w *fileWriter) Write(p []byte) (int, error) {
	if w.closed {
		return 0, errors.New("zip: write to closed file")
	}
	w.crc32.Write(p)
	return w.rawCount.Write(p)
}

func (w *fileWriter) close() error {
	if w.closed {
		return errors.New("zip: file closed twice")
	}
	w.closed = true
	if err := w.comp.Close(); err != nil {
		return err
	}

	// 更新文件头
	fh := w.header.FileHeader
	fh.CRC32 = w.crc32.Sum32()
	fh.CompressedSize64 = uint64(w.compCount.count)
	fh.UncompressedSize64 = uint64(w.rawCount.count)

	if fh.isZip64() {
		fh.CompressedSize = uint32max
		fh.UncompressedSize = uint32max
		fh.ReaderVersion = zipVersion45 // 需要4.5-文件使用ZIP64格式扩展名
	} else {
		fh.CompressedSize = uint32(fh.CompressedSize64)
		fh.UncompressedSize = uint32(fh.UncompressedSize64)
	}

	// 写入数据描述符。这比我们想象的要复杂得多
	// 想想看，比如zipfile.c中的注释：putextended（）和
	// http:
	// 这里的方法是在需要时写入8字节大小，而不使用
	// 在本地头中添加zip64额外的（无论如何都太迟了）。
	var buf []byte
	if fh.isZip64() {
		buf = make([]byte, dataDescriptor64Len)
	} else {
		buf = make([]byte, dataDescriptorLen)
	}
	b := writeBuf(buf)
	b.uint32(dataDescriptorSignature) // OSX要求的事实标准
	b.uint32(fh.CRC32)
	if fh.isZip64() {
		b.uint64(fh.CompressedSize64)
		b.uint64(fh.UncompressedSize64)
	} else {
		b.uint32(fh.CompressedSize)
		b.uint32(fh.UncompressedSize)
	}
	_, err := w.zipw.Write(buf)
	return err
}

type countWriter struct {
	w     io.Writer
	count int64
}

func (w *countWriter) Write(p []byte) (int, error) {
	n, err := w.w.Write(p)
	w.count += int64(n)
	return n, err
}

type nopCloser struct {
	io.Writer
}

func (w nopCloser) Close() error {
	return nil
}

type writeBuf []byte

func (b *writeBuf) uint8(v uint8) {
	(*b)[0] = v
	*b = (*b)[1:]
}

func (b *writeBuf) uint16(v uint16) {
	binary.LittleEndian.PutUint16(*b, v)
	*b = (*b)[2:]
}

func (b *writeBuf) uint32(v uint32) {
	binary.LittleEndian.PutUint32(*b, v)
	*b = (*b)[4:]
}

func (b *writeBuf) uint64(v uint64) {
	binary.LittleEndian.PutUint64(*b, v)
	*b = (*b)[8:]
}
